<?php

/**
 * Htmlier : parse HTML markup, strip unwanted tags/attributes,
 * escape special chars in text
 *
 * @author vbolshov
 */

namespace km\util;

use \DOMDocument;

class Htmlier {
	/**
	 * @var bool
	 */
	private $typography = true,
		$hyperlinks = true;
	/**
	 * @var string @see http://php.net/strip_tags
	 */
	private $allow_tags = '<a><b><i><em><strong><p><blockquote><img><pre>';
	/**
	 * @var string[]
	 */
	private $disallow_attrs = array(
		'style', 
	);
	/**
	 * @var string[]
	 */
	private $disallow_attrs_re = array(
		'/^on.+/i',
	);
	/**
	 * @var string
	 */
	private $xss_replacer = ' I just tryed to perform a XSS attack on this site. Somebody kill me. ';
	/**
	 * @return string
	 */
	function getXssReplacer()
	{
		return $this->xss_replacer;
	}
	/**
	 * @param string $replacer
	 */
	function setXssReplacer($replacer)
	{
		$this->xss_replacer = $replacer;
	}
	/**
	 * @param bool $typography
	 */
	function setTypograhy($typography)
	{
		$this->typography = (bool) $typography;
	}
	/**
	 * @param bool $hyperlinks
	 */
	function setHyperlinks($hyperlinks)
	{
		$this->hyperlinks = (bool) $hyperlinks;
	}
	/**
	 * @param string $tags @see http://php.net/strip_tags
	 */
	function setAllowedTags($tags)
	{
		$this->allow_tags = $tags;
	}
	/**
	 * @param string[] $attrs
	 */
	function setDisallowedAttributes(array $attrs)
	{
		$this->disallow_attrs = $attrs;
	}
	/**
	 * @param string[] $attrs
	 */
	function setDisallowedAttributeRegexps(array $attrs)
	{
		$this->disallow_attrs_re = $attrs;
	}
	/**
	 * The main method: filter HTML, strip unwanted tags and attributes
	 *
	 * @param string $html
	 * @return string
	 */
	function filter($html)
	{
		if ($this->typography) {
			$html = str_replace(array('<-', '->', ' - '), array('&larr;', '&rarr;', ' &mdash; '), $html);
		}
		
		/**
		 * text inside <pre></pre> should be converted to htmlspecialchars
		 */
		$html	= preg_replace_callback("/(\<pre(?: [^\>]*)?\>)(.+)(\<\/pre(?: [^\>]*)?\>)/iU", function($m) {
			return $m[1] . htmlspecialchars($m[2]) . $m[3];
		}, $html);
		
		$html = str_replace(array(' < ', ' > '), array(' &lt; ', '&gt;'), $html);
		
		$html	= preg_replace("/ (\<) (?![a-z\/]) /xi", "&lt;", $html);
		$html	= preg_replace("/ (\<) (?![^\<]* \>) /x", "&lt;", $html);
		
		$html	= strip_tags($html, $this->allow_tags);
		$html	= nl2br($html);
		
		$html	= preg_replace("/ (\<) (?![a-z\/]) /xi", "&lt;", $html);
		
		if ($this->hyperlinks) {
			// scheme
			// subdomain(s)
			// domain
			// path
			// query
			// fragment
			$html	= preg_replace("/ ( 
				[a-z0-9]+\:\/\/ 
				
				(?:[a-z0-9\-]+\.)* 
				
				[a-z0-9\-]+\.[a-z]+ 
				
				(?:
					\/[a-z0-9_\-\.%]+
				)*
				
				\/?
				
				(?: 
					\? 
					(?: 
						[a-z0-9_\-%]* 
						(?: [a-z0-9_\-%\[\]]* )* 
						(?: = [a-z0-9_\-%\[\]]*)
						(?: &(?:amp;)? )?
					)*
				)?
				
				(?: \#[a-z0-9\-\_\[\]]* )?  
			) /xi", '<a href="\\1">\\1</a>', $html);
		}
		
		$dom = new DOMDocument();
		@$dom->loadHTML($html);
		
		$tags = explode('><', trim($this->allow_tags, '<>'));
		foreach ($tags as $tag) {
			foreach ($dom->getElementsByTagName($tag) as $node) {
				foreach ($node->attributes as $attr) {
					if ($this->isForbidden($attr->name)) {
						$node->removeAttribute($attr->name);
					} elseif (('href' == strtolower($attr->name)) && 
						(false !== stripos($attr->value, 'script:')))
					{// anti XSS filter
						$node->parentNode->replaceChild($dom->createTextNode($this->xss_replacer), $node);
					}
				}
			}
		}
		
		$ret = str_replace(
			array(
				'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">',
				'<html><body>', '</body></html>'), 
			"", 
			$dom->saveHTML()
		);
		
		return trim($ret);
	}
	private function isForbidden($attr)
	{
		if (in_array($attr, $this->disallow_attrs)) {
			return true;
		}
		foreach ($this->disallow_attrs_re as $pattern) {
			if (preg_match($pattern, $attr)) {
				return true;
			}
		}
		return false;
	}
}
